Bienvenidos a este tutorial, en el que voy a hacer una introducción al tidyverse y como utilizarlo para hacer una de las tareas más tediosas: limpiar datos. 1

El tidyverse

Tidyverse es una colección de paquetes diseñados para ayudar a los usuarios de datos a trabajar de manera más eficiente con sus datos. La mayoría de las funciones de tidyverse tienen funciones equivalentes en base R (es decir, el conjunto de funciones que se cargan automáticamente cuando se inicia R). La diferencia es que las funciones de tidyverse suelen ser más rápidas, la sintaxis puede ser, en general, más clara y fácil de usar y, lo que es más importante, todas siguen una estructura similar y los resultados son consistentes entre paquetes (dentro de los límites, esto depende de la naturaleza de la función).

Nota técnica: tidyverse utiliza principios de “evaluación no estándar” (NSE) en sus funciones. NSE es un tema complicado, pero, en resumen, significa que los paquetes en tidyverse siguen reglas un poco diferentes a la función base R. Las funciones de tidyverse son más fáciles de usar, pero, si está acostumbrado a las funciones R básicas, le costará un poco de esfuerzo reorientar su cerebro de programación R. ¡Solo una advertencia amistosa antes de sumergirnos!

Paquetes en el tidyverse

Como dije anteriormente, tidyverse es una colección de muchos paquetes. El core tidyverse incluye los paquetes que probablemente usará en los análisis de datos cotidianos. A partir detidyverse1.3.0, los siguientes paquetes se incluyen en el core tidyverse:

Paquete Propósito
dplyr para organizar datos
ggplot2 para generar visualizaciones
tidyr para reestructurar datos.
readr para leer datos tabulares
purrr transmisión avanzada de funciones a través de datos
tibble para manejar datos en forma tabular
stringr para manejar “strings” (variables de caracteres)
forcats para manejar datos categóricos

Llamando el paquete

Para llamar el paquete podemos usar las funciones library(), require(), o si usamos el paquete rio, p_load(). Usemos por ahora la función library()

library("tidyverse")
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.2 ──
## ✔ ggplot2 3.3.6      ✔ purrr   0.3.5 
## ✔ tibble  3.1.8      ✔ dplyr   1.0.10
## ✔ tidyr   1.2.1      ✔ stringr 1.4.1 
## ✔ readr   2.1.2      ✔ forcats 0.5.2 
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()

El mensaje indica los paquetes que fueron cargados, y los mensajes de advertencia indican que hay nombres de función idénticos en diferentes paquetes. El último paquete cargado anulará las funciones de los paquetes cargados anteriormente cuando surjan tales conflictos. Por lo tanto, el orden en que se cargan los paquetes puede ser importante. Sin embargo, se puede acceder a cualquier función agregando un prefijo del nombre del paquete y dos puntos a una función en particular.

Un Ejemplo Sencillo: Composición demográfica y rendimiento académico

En este tutorial, vamos a utilizar datos reales obtenidos del portal de datos abiertos de la ciudad de Nueva York y analizar si existe una correlación entre la composición demográfica de los High Schools de la ciudad y los resultados de las pruebas estandarizadas de Aptitud Académica (SAT) que toman la mayoría de los estudiantes de secundaria en los Estados Unidos

Para nuestro análisis vamos a utilizar dos bases de datos una que contiene la información demográfica y otra que contiene los resultados del SAT:

  • demog.csv: contiene datos en formato .csv sobre la demografía de todos los estudiantes de la ciudad de Nueva York para los años que van desde 2006 a 2012. Estos datos incluyen información sobre la raza y el sexo, entre otros. Los datos fueron descargados el 01/12/2023 del siguiente link

  • 2012_SAT_Results.csv: contiene datos en formato .csv sobre los puntajes de la Pruebas de Aptitud Académica (SAT). Los datos contienen información de las calificaciones promedio en cada una de las secciones de las pruebas: lectura crítica, matemáticas y escritura. Los datos fueron descargados el 01/12/2023 del siguiente link

Cargando datos

Para leer estos datos en formato .csv podemos utilizar dos funciones: read.csv() que pertenece a R base y read_csv() que pertenece al paquete readr del universo tidyverse

sat <- read.csv("https://raw.githubusercontent.com/ignaciomsarmiento/datasets/main/2012_SAT_Results.csv")
demog<- read_csv("https://raw.githubusercontent.com/ignaciomsarmiento/datasets/main/demog.csv")

¿En qué se diferencian las funciones read.csv() (de base R) y read_csv (de readr)? La respuesta tiene que ver con el tipo de objeto que importa cada función, mientras que read_csv() importa datos en R como un tibble, read.csv() importa un data.frame de R base.

Esto podemos verlo usando la función str()

str(sat)
## 'data.frame':    478 obs. of  6 variables:
##  $ DBN                            : chr  "01M292" "01M448" "01M450" "01M458" ...
##  $ SCHOOL.NAME                    : chr  "HENRY STREET SCHOOL FOR INTERNATIONAL STUDIES" "UNIVERSITY NEIGHBORHOOD HIGH SCHOOL" "EAST SIDE COMMUNITY SCHOOL" "FORSYTH SATELLITE ACADEMY" ...
##  $ Num.of.SAT.Test.Takers         : chr  "29" "91" "70" "7" ...
##  $ SAT.Critical.Reading.Avg..Score: chr  "355" "383" "377" "414" ...
##  $ SAT.Math.Avg..Score            : chr  "404" "423" "402" "401" ...
##  $ SAT.Writing.Avg..Score         : chr  "363" "366" "370" "359" ...
str(demog)
## spec_tbl_df [10,075 × 38] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ DBN              : chr [1:10075] "01M015" "01M015" "01M015" "01M015" ...
##  $ Name             : chr [1:10075] "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" ...
##  $ schoolyear       : num [1:10075] 20052006 20062007 20072008 20082009 20092010 ...
##  $ fl_percent       : chr [1:10075] "89.4" "89.4" "89.4" "89.4" ...
##  $ frl_percent      : num [1:10075] NA NA NA NA 96.5 96.5 89.4 NA NA NA ...
##  $ total_enrollment : num [1:10075] 281 243 261 252 208 203 189 402 312 338 ...
##  $ prek             : num [1:10075] 15 15 18 17 16 13 13 15 13 28 ...
##  $ k                : num [1:10075] 36 29 43 37 40 37 31 43 37 48 ...
##  $ grade1           : num [1:10075] 40 39 39 44 28 35 35 55 45 46 ...
##  $ grade2           : num [1:10075] 33 38 36 32 32 33 28 53 52 47 ...
##  $ grade3           : num [1:10075] 38 34 38 34 30 30 25 68 47 53 ...
##  $ grade4           : num [1:10075] 52 42 47 39 24 30 28 59 61 48 ...
##  $ grade5           : num [1:10075] 29 46 40 49 38 25 29 64 57 68 ...
##  $ grade6           : num [1:10075] 38 NA NA NA NA NA NA 45 NA NA ...
##  $ grade7           : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade8           : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade9           : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade10          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade11          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade12          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ ell_num          : num [1:10075] 36 38 52 48 40 30 20 37 30 40 ...
##  $ ell_percent      : num [1:10075] 12.8 15.6 19.9 19 19.2 14.8 10.6 9.2 9.6 11.8 ...
##  $ sped_num         : num [1:10075] 57 55 60 62 46 46 40 93 72 75 ...
##  $ sped_percent     : num [1:10075] 20.3 22.6 23 24.6 22.1 22.7 21.2 23.1 23.1 22.2 ...
##  $ ctt_num          : num [1:10075] 25 19 20 21 14 21 23 7 13 12 ...
##  $ selfcontained_num: num [1:10075] 9 15 14 17 14 9 7 37 22 19 ...
##  $ asian_num        : num [1:10075] 10 18 16 16 16 13 12 40 30 42 ...
##  $ asian_per        : num [1:10075] 3.6 7.4 6.1 6.3 7.7 6.4 6.3 10 9.6 12.4 ...
##  $ black_num        : num [1:10075] 74 68 77 75 67 75 63 103 70 72 ...
##  $ black_per        : num [1:10075] 26.3 28 29.5 29.8 32.2 36.9 33.3 25.6 22.4 21.3 ...
##  $ hispanic_num     : num [1:10075] 189 153 157 149 118 110 109 207 172 186 ...
##  $ hispanic_per     : num [1:10075] 67.3 63 60.2 59.1 56.7 54.2 57.7 51.5 55.1 55 ...
##  $ white_num        : num [1:10075] 5 4 7 7 6 4 4 39 19 22 ...
##  $ white_per        : num [1:10075] 1.8 1.6 2.7 2.8 2.9 2 2.1 9.7 6.1 6.5 ...
##  $ male_num         : num [1:10075] 158 140 143 149 124 113 97 214 157 162 ...
##  $ male_per         : num [1:10075] 56.2 57.6 54.8 59.1 59.6 55.7 51.3 53.2 50.3 47.9 ...
##  $ female_num       : num [1:10075] 123 103 118 103 84 90 92 188 155 176 ...
##  $ female_per       : num [1:10075] 43.8 42.4 45.2 40.9 40.4 44.3 48.7 46.8 49.7 52.1 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   DBN = col_character(),
##   ..   Name = col_character(),
##   ..   schoolyear = col_double(),
##   ..   fl_percent = col_character(),
##   ..   frl_percent = col_double(),
##   ..   total_enrollment = col_double(),
##   ..   prek = col_double(),
##   ..   k = col_double(),
##   ..   grade1 = col_double(),
##   ..   grade2 = col_double(),
##   ..   grade3 = col_double(),
##   ..   grade4 = col_double(),
##   ..   grade5 = col_double(),
##   ..   grade6 = col_double(),
##   ..   grade7 = col_double(),
##   ..   grade8 = col_double(),
##   ..   grade9 = col_double(),
##   ..   grade10 = col_double(),
##   ..   grade11 = col_double(),
##   ..   grade12 = col_double(),
##   ..   ell_num = col_double(),
##   ..   ell_percent = col_double(),
##   ..   sped_num = col_double(),
##   ..   sped_percent = col_double(),
##   ..   ctt_num = col_double(),
##   ..   selfcontained_num = col_double(),
##   ..   asian_num = col_double(),
##   ..   asian_per = col_double(),
##   ..   black_num = col_double(),
##   ..   black_per = col_double(),
##   ..   hispanic_num = col_double(),
##   ..   hispanic_per = col_double(),
##   ..   white_num = col_double(),
##   ..   white_per = col_double(),
##   ..   male_num = col_double(),
##   ..   male_per = col_double(),
##   ..   female_num = col_double(),
##   ..   female_per = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>

Un poco sobre tibbles

Un tibble es un tipo de objeto creado para tidyverse. Cuando utilizamos readr para leer datos los importa con este formato, y este formato es el que devuelven muchas funciones de tidyverse.

El tibble es como data.frame de R base con algunas otras reglas. Por ejemplo, tiene una función de impresión simplificada que si uno accidentalmente escribe el nombre del objeto: solo imprimirá las primeras 10 líneas y truncará el número de columnas para que se muestren en su pantalla. Si escribe el nombre del objeto de un data.frame tradicional, ¡se imprimen las primeras 1000 líneas de datos!

Para crear un nuevo tibble, puede usar la función tribble() para crear un tibble por filas, o la función tibble() para construir un tibble en columnas, o as.tibble() para convertir un objeto en un tibble.

Hay algunos aspectos de tibbles que son fundamentalmente diferentes de un data.frame estándar. Tibbles no maneja bien los nombres de las filas y muchas funciones de tidyverse descartan esa información. Esto a menudo está bien: los nombres de fila son computacionalmente costosos de establecer y mantener. Además, puede ser difícil filtrar y ordenar los datos según los nombres de las filas. Sin embargo, otros paquetes (que no pertenecen al tidyverse) se basan en el atributo de nombres de fila, así que tenga en cuenta este comportamiento cuando utilice distintos paquetes.

Los tibbles tienen también algunas ventajas sobre los data.frame tradicionales:

  • cargan más rápido
  • permitirle tener listas en las columnas
  • permitir nombres de variables no estándar (es decir, sus variables pueden comenzar con un número y pueden contener espacios)

Seleccionando y filtrando los datos

Para poder analizar la correlación que existe entre la composición demográfica de las escuelas y los resultados en el SAT, necesitamos entonces “limpiar” nuestros datos.

Seleccionando variables (columnas)

Comenzamos primero seleccionando las variables que son de interés para llevar a cabo nuestro análisis. Para seleccionar variables podemos utilizar la función select() del paquete dplyr. Esta se usa para la seleccionar columnas y tiene la estructura general: select(data, variables_to_select).

Por ejemplo, en este análisis queremos retener variables que brindan información sobre porcentajes de estudiantes de diferentes razas e información en que grado están:

demog <- demog %>%
  select(DBN, Name, schoolyear, frl_percent, total_enrollment, asian_per, black_per, hispanic_per, white_per,starts_with("grade"),selfcontained_num)

str(demog) 
## tibble [10,075 × 22] (S3: tbl_df/tbl/data.frame)
##  $ DBN              : chr [1:10075] "01M015" "01M015" "01M015" "01M015" ...
##  $ Name             : chr [1:10075] "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" ...
##  $ schoolyear       : num [1:10075] 20052006 20062007 20072008 20082009 20092010 ...
##  $ frl_percent      : num [1:10075] NA NA NA NA 96.5 96.5 89.4 NA NA NA ...
##  $ total_enrollment : num [1:10075] 281 243 261 252 208 203 189 402 312 338 ...
##  $ asian_per        : num [1:10075] 3.6 7.4 6.1 6.3 7.7 6.4 6.3 10 9.6 12.4 ...
##  $ black_per        : num [1:10075] 26.3 28 29.5 29.8 32.2 36.9 33.3 25.6 22.4 21.3 ...
##  $ hispanic_per     : num [1:10075] 67.3 63 60.2 59.1 56.7 54.2 57.7 51.5 55.1 55 ...
##  $ white_per        : num [1:10075] 1.8 1.6 2.7 2.8 2.9 2 2.1 9.7 6.1 6.5 ...
##  $ grade1           : num [1:10075] 40 39 39 44 28 35 35 55 45 46 ...
##  $ grade2           : num [1:10075] 33 38 36 32 32 33 28 53 52 47 ...
##  $ grade3           : num [1:10075] 38 34 38 34 30 30 25 68 47 53 ...
##  $ grade4           : num [1:10075] 52 42 47 39 24 30 28 59 61 48 ...
##  $ grade5           : num [1:10075] 29 46 40 49 38 25 29 64 57 68 ...
##  $ grade6           : num [1:10075] 38 NA NA NA NA NA NA 45 NA NA ...
##  $ grade7           : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade8           : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade9           : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade10          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade11          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade12          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ selfcontained_num: num [1:10075] 9 15 14 17 14 9 7 37 22 19 ...

El símbolo %>% es lo que se conoce como un operador de pipa (pipe operator) y viene como parte de tidyverse. Este nos permite concatenar comandos y funciones, diciendo básicamente que al elemento anterior se le realice la operación siguiente. En nuestro caso le estamos diciendo a R que del objeto demog seleccione ciertas variables. No es necesario usar este operador pero simplifica y mejora la legibilidad del código. Podríamos haber hecho la misma operación sin este operador y este luciría así:

demog <- select(demog, DBN, Name, schoolyear, frl_percent, total_enrollment, asian_per, black_per, hispanic_per, white_per,starts_with("grade"),selfcontained_num)

select() contiene además una función muy útil que selecciona variables que comienzan (starts_with()) o finalizan (ends_with()) con cierto patrón.

Por error en el paso anterior incluí la variable selfcontained_num que identifica el **Número total de estudiantes en clases especiales* que no sirve para el análisis, puedo removerla con la misma función más un signo menos (-) antes de la variable que quiero eliminar:

demog <- demog %>% 
    select (-selfcontained_num)

str(demog) 
## tibble [10,075 × 21] (S3: tbl_df/tbl/data.frame)
##  $ DBN             : chr [1:10075] "01M015" "01M015" "01M015" "01M015" ...
##  $ Name            : chr [1:10075] "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" "P.S. 015 ROBERTO CLEMENTE" ...
##  $ schoolyear      : num [1:10075] 20052006 20062007 20072008 20082009 20092010 ...
##  $ frl_percent     : num [1:10075] NA NA NA NA 96.5 96.5 89.4 NA NA NA ...
##  $ total_enrollment: num [1:10075] 281 243 261 252 208 203 189 402 312 338 ...
##  $ asian_per       : num [1:10075] 3.6 7.4 6.1 6.3 7.7 6.4 6.3 10 9.6 12.4 ...
##  $ black_per       : num [1:10075] 26.3 28 29.5 29.8 32.2 36.9 33.3 25.6 22.4 21.3 ...
##  $ hispanic_per    : num [1:10075] 67.3 63 60.2 59.1 56.7 54.2 57.7 51.5 55.1 55 ...
##  $ white_per       : num [1:10075] 1.8 1.6 2.7 2.8 2.9 2 2.1 9.7 6.1 6.5 ...
##  $ grade1          : num [1:10075] 40 39 39 44 28 35 35 55 45 46 ...
##  $ grade2          : num [1:10075] 33 38 36 32 32 33 28 53 52 47 ...
##  $ grade3          : num [1:10075] 38 34 38 34 30 30 25 68 47 53 ...
##  $ grade4          : num [1:10075] 52 42 47 39 24 30 28 59 61 48 ...
##  $ grade5          : num [1:10075] 29 46 40 49 38 25 29 64 57 68 ...
##  $ grade6          : num [1:10075] 38 NA NA NA NA NA NA 45 NA NA ...
##  $ grade7          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade8          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade9          : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade10         : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade11         : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...
##  $ grade12         : num [1:10075] NA NA NA NA NA NA NA NA NA NA ...

Ordenando los datos

Para ordenar los datos podemos utilizar la función arrange() que ordena de mayor a menor por default:

demog <- demog %>% arrange(total_enrollment)
head(demog)
## # A tibble: 6 × 21
##   DBN    Name     schoo…¹ frl_p…² total…³ asian…⁴ black…⁵ hispa…⁶ white…⁷ grade1
##   <chr>  <chr>      <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>  <dbl>
## 1 02M445 SEWARD …  2.01e7      NA       1       0     100       0       0     NA
## 2 12X191 IS 191    2.01e7      NA       1       0     100       0       0     NA
## 3 12X234 PS 234 …  2.01e7      NA       1       0       0     100       0     NA
## 4 17K479 ERASMUS…  2.01e7      NA       1       0     100       0       0     NA
## 5 27Q180 MS 180 …  2.01e7      NA       1       0       0       0     100     NA
## 6 06M090 I S 090…  2.01e7      NA       2       0       0     100       0     NA
## # … with 11 more variables: grade2 <dbl>, grade3 <dbl>, grade4 <dbl>,
## #   grade5 <dbl>, grade6 <dbl>, grade7 <dbl>, grade8 <dbl>, grade9 <dbl>,
## #   grade10 <dbl>, grade11 <dbl>, grade12 <dbl>, and abbreviated variable names
## #   ¹​schoolyear, ²​frl_percent, ³​total_enrollment, ⁴​asian_per, ⁵​black_per,
## #   ⁶​hispanic_per, ⁷​white_per

o podemos hacerlo de manera descendente con la función desc():

demog <- demog %>% arrange(desc(total_enrollment), desc(white_per))
head(demog)
## # A tibble: 6 × 21
##   DBN    Name     schoo…¹ frl_p…² total…³ asian…⁴ black…⁵ hispa…⁶ white…⁷ grade1
##   <chr>  <chr>      <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>  <dbl>
## 1 13K430 BROOKLY…  2.01e7    50.7    5332    60.3    10.2     7.9    21.3     NA
## 2 13K430 BROOKLY…  2.01e7    64.3    5140    60.4    10.7     7.7    21       NA
## 3 13K430 BROOKLY…  2.01e7    53.8    4947    58.8    12       8.3    20.5     NA
## 4 13K430 BROOKLY…  2.01e7    NA      4662    57.4    13.1     8      21       NA
## 5 20K490 FORT HA…  2.01e7    NA      4538    28.2     5.2    32.3    31.8     NA
## 6 10X440 DEWITT …  2.01e7    NA      4533     5.7    26      63.5     3.4     NA
## # … with 11 more variables: grade2 <dbl>, grade3 <dbl>, grade4 <dbl>,
## #   grade5 <dbl>, grade6 <dbl>, grade7 <dbl>, grade8 <dbl>, grade9 <dbl>,
## #   grade10 <dbl>, grade11 <dbl>, grade12 <dbl>, and abbreviated variable names
## #   ¹​schoolyear, ²​frl_percent, ³​total_enrollment, ⁴​asian_per, ⁵​black_per,
## #   ⁶​hispanic_per, ⁷​white_per

Seleccionando filas

A menudo también es importante seleccionar filas, una forma es elegirlas basado en su orden. Para ello la función slice() es útil ya que nos permite seleccionar un número específico. Por ejemplo, si queremos seleccionar las primeras 5 filas:

demog_5 <- demog %>% slice(1:5)
head(demog_5)
## # A tibble: 5 × 21
##   DBN    Name     schoo…¹ frl_p…² total…³ asian…⁴ black…⁵ hispa…⁶ white…⁷ grade1
##   <chr>  <chr>      <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>  <dbl>
## 1 13K430 BROOKLY…  2.01e7    50.7    5332    60.3    10.2     7.9    21.3     NA
## 2 13K430 BROOKLY…  2.01e7    64.3    5140    60.4    10.7     7.7    21       NA
## 3 13K430 BROOKLY…  2.01e7    53.8    4947    58.8    12       8.3    20.5     NA
## 4 13K430 BROOKLY…  2.01e7    NA      4662    57.4    13.1     8      21       NA
## 5 20K490 FORT HA…  2.01e7    NA      4538    28.2     5.2    32.3    31.8     NA
## # … with 11 more variables: grade2 <dbl>, grade3 <dbl>, grade4 <dbl>,
## #   grade5 <dbl>, grade6 <dbl>, grade7 <dbl>, grade8 <dbl>, grade9 <dbl>,
## #   grade10 <dbl>, grade11 <dbl>, grade12 <dbl>, and abbreviated variable names
## #   ¹​schoolyear, ²​frl_percent, ³​total_enrollment, ⁴​asian_per, ⁵​black_per,
## #   ⁶​hispanic_per, ⁷​white_per

Filtrando datos

Para nuestro análisis necesitamos la información de los High Schools. Sin embargo, la base con los datos demográficos incluye información de todos los colegios de la ciudad, independientemente si son High Schools o no. Para filtrar solo los High Schools vamos a usar la información de que las insituciones con individuos en los grados 9 a 12 son instituciones secundarias. Por lo tanto, si hay información faltante en esta variable, es porque no son High Schools. Esto lo podemos hacer usando la función filter() que sigue la estructura: filter(data, condition_for_filter):

demog <- demog %>% filter(grade9 != "NA")

Es decir, nos quedamos con observaciones donde que no tienen valores faltantes en la variable de interés: grade9. Podemos también filtrar usando varias condiciones, por ejemplo:

demog <- demog %>% filter(grade9 != "NA", schoolyear == "20112012")

que agrega la condición de que los datos pertenezcan al año escolar 2011-2012. Esto será importante porque vamos a unir esta base con los resultados del SAT en 2012. Podríamos también filtrar utilizando el criterio de que coincidan con múltiples valores. El operador %in% es extremadamente valioso en estos casos:

demog_sub <- demog %>% filter(Name %in% c("WILLIAMSBURG PREPARATORY SCHOOL","THE SCHOOL FOR HUMAN RIGHTS","THE HIGH SCHOOL FOR GLOBAL CITIZENSHIP"))
table(demog_sub$Name)
## 
## THE HIGH SCHOOL FOR GLOBAL CITIZENSHIP            THE SCHOOL FOR HUMAN RIGHTS 
##                                      1                                      1 
##        WILLIAMSBURG PREPARATORY SCHOOL 
##                                      1

Transformando los datos

Con las variables y las observaciones de interés seleccionadas y filtradas podemos comenzar a hacer operaciones sobre las columnas.

Renombrando las variables

Por ejemplo, podemos querer cambiar el nombre de un variable. La función rename() cambia el nombre de las variables usando la siguiente notación: rename(dataframe, newname = "oldname"):

demog <- demog %>% rename(AnoEscolar= schoolyear)

En este caso cambiamos el nombre de la variable schoolyear a AnoEscolar.

Creando nuevas variables

Podemos también querer crear una nueva variable. Aquí utilizamos la función mutate(). Esta función sirve para crear (mutar) nuevas variables, y tiene la sintaxis mutate(data, new_variable_name = data_transformation):

Supongamos que queremos verificar que los porcentajes de la composición racial suman 100:

demog <- demog %>% mutate(perc_total= asian_per+black_per+hispanic_per+white_per)

y con la función summary() de R base podemos ver las estadísticas descriptivas de esta nueva variable:

summary(demog$perc_total)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   88.40   99.00   99.50   99.16   99.80  100.10

Reestructurando los datos

Ocasionalmente, tendremos datos que necesitan pasar de formato horizontal (wide) a uno vertical (long) o viceversa. Esto es a veces necesario para graficar, para comparar puntos de datos particulares, o porque otro paquete de R lo requiere. Tidyr contiene varias funciones que nos permiten hacer esto sin esfuerzo. Las dos funciones claves son pivot_wider() para pasar de formato long a wide y pivot_longer() de wide a long.

wide_to_long

Ilustremos esto en nuestro ejemplo, pero también aprovecharemos para mostrar el poder de concatenar funciones del operador de pipa o pipe operator. Vamos a transformar nuestros datos que está en formato horizontal o wide respecto a la composición racial y lo pasaremos al formato vertical o long:

demog_long <- demog %>% 
              select(DBN, AnoEscolar,Name,asian_per,black_per,hispanic_per,white_per) %>% 
              pivot_longer(cols = c(asian_per,black_per,hispanic_per,white_per),
                                     names_to = "Race",
                                     values_to = "Perc", 
                                     values_drop_na = T)
head(demog_long)
## # A tibble: 6 × 5
##   DBN    AnoEscolar Name                           Race          Perc
##   <chr>       <dbl> <chr>                          <chr>        <dbl>
## 1 13K430   20112012 BROOKLYN TECHNICAL HIGH SCHOOL asian_per     60.3
## 2 13K430   20112012 BROOKLYN TECHNICAL HIGH SCHOOL black_per     10.2
## 3 13K430   20112012 BROOKLYN TECHNICAL HIGH SCHOOL hispanic_per   7.9
## 4 13K430   20112012 BROOKLYN TECHNICAL HIGH SCHOOL white_per     21.3
## 5 20K490   20112012 FORT HAMILTON HIGH SCHOOL      asian_per     30.3
## 6 20K490   20112012 FORT HAMILTON HIGH SCHOOL      black_per      5

Concatenamos entonces dos funciones, select() y pivot_longer(), creando un nuevo objeto con los datos demográficos para cada escuela en formato long. Al hacerlo enviamos el nombre de las variables a una nueva variable llamada Race y los valores de estas variables a una nueva que le llamamos Perc.

Agrupando y resumiendo los datos

Otra tarea que solemos realizar mucho es resumir información por grupos. Supongamos que queremos colapsar (resumir) la base anterior (demog_long) en el porcentaje total de las composiciones raciales.

La función group_by() proporciona una herramienta útil para dividir los datos en grupos según un factor común y la función summarize() para aplicar una función de resumen a esos datos. Colapsemos entonces por escuela y año escolar la suma de los porcentajes de la composición racial de cada una de ellas:

demog_long_sum <- demog_long %>% 
              group_by(DBN, AnoEscolar,Name) %>% 
             summarize(perc_total=sum(Perc))

Si hacemos el resumen podemos ver que coincide con la variable que creamos anteriormente que era la suma horizontal de estas composiciones:

summary(demog_long_sum$perc_total)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   88.40   99.00   99.50   99.16   99.80  100.10
summary(demog$perc_total)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   88.40   99.00   99.50   99.16   99.80  100.10

Uniendo bases de datos

Poder unir distintas bases de datos utilizando una variable en común es una de las tareas más comunes en la limpieza de datos. Para poder unir dos bases de datos, es importante tener una variable en común que pueda ser automáticamente detectada o que se tenga que definir explícitamente.

La forma general de las función que sirven para unir bases en tidyverse es type_join(x_data, y_data, ID_key_variable). Cuando unimos los datos la(s) columna(s) usada(s) como identificadores (ID_key_variable) solo se incluyen una vez en los datos fusionados finales, es decir, la(s) columna(s) de clave duplicada(s) se descarta(n). Sin embargo, si hay otras columnas (sin ID) con nombres duplicados en los datos, las columnas duplicadas del primer conjunto de datos (x_data) reciben un sufijo .x y las columnas duplicadas del segundo conjunto de datos (y_data) reciben un sufijo .y.

Dependiendo de la función que se utilice la base resultante contara con todas las columnas de ambas bases o un subconjunto de ellas. Los seis tipos incluidos en tidyverse son:

  1. El full_join(): es la opción más segura para evitar la eliminación de datos, devuelve todo.

  2. El inner_join(): sólo mantiene lo que es común entre los conjuntos de datos.

  3. El left_join(): une todas las filas de las observaciones que se encuentran en el tibble declarado a la izquierda o en primer lugar.

  4. El right_join(): une todas las filas de las observaciones que se encuentran en el tibble declarado a la derecha o en segundo lugar.

  5. El semi_join(): es como inner_join(), pero sólo se conservan las filas con claves en ambos conjuntos de datos, excepto que no conserva ningún dato del conjunto de datos que aparece a la derecha o en segundo lugar.

  6. El anti_join(): conserva las observaciones del primer data.frame que no coinciden con el segundo .

Uniendo las bases del análisis

El último paso que nos queda antes de poder hacer nuestro análisis de correlación es unir las dos bases de datos.

La base del SAT tiene seis variables que, en conjunto, identifican a cada escuela y brindan información sobre los puntajes promedio que obtuvieron los estudiantes en cada escuela secundaria en las tres secciones del SAT: lectura crítica, matemáticas y escritura. Todas las variables son relevantes para nuestro propósito de comprender el efecto de la demografía en el rendimiento de la prueba y deben conservarse.

La variable DBN, es un identificador de las escuelas y es común a ambas bases, la utilizaremos entonces como nuestra ID_key_variable:

sat_demog_dir <- sat %>%
  left_join(demog, by = c("DBN"))

str(sat_demog_dir)
## 'data.frame':    478 obs. of  27 variables:
##  $ DBN                            : chr  "01M292" "01M448" "01M450" "01M458" ...
##  $ SCHOOL.NAME                    : chr  "HENRY STREET SCHOOL FOR INTERNATIONAL STUDIES" "UNIVERSITY NEIGHBORHOOD HIGH SCHOOL" "EAST SIDE COMMUNITY SCHOOL" "FORSYTH SATELLITE ACADEMY" ...
##  $ Num.of.SAT.Test.Takers         : chr  "29" "91" "70" "7" ...
##  $ SAT.Critical.Reading.Avg..Score: chr  "355" "383" "377" "414" ...
##  $ SAT.Math.Avg..Score            : chr  "404" "423" "402" "401" ...
##  $ SAT.Writing.Avg..Score         : chr  "363" "366" "370" "359" ...
##  $ Name                           : chr  "HENRY STREET SCHOOL FOR INTERNATIONAL STUDIES" "UNIVERSITY NEIGHBORHOOD HIGH SCHOOL" "EAST SIDE COMMUNITY HIGH SCHOOL" "SATELLITE ACADEMY HS @ FORSYTHE STREET" ...
##  $ AnoEscolar                     : num  20112012 20112012 20112012 20112012 20112012 ...
##  $ frl_percent                    : num  88.6 71.8 71.8 72.8 80.7 NA 23 69.8 18 66.9 ...
##  $ total_enrollment               : num  422 394 598 224 367 ...
##  $ asian_per                      : num  14 29.2 9.7 2.2 9.3 NA 27.8 0.5 15.1 1.7 ...
##  $ black_per                      : num  29.1 22.6 23.9 34.4 31.6 NA 11.7 45.4 15.1 32.2 ...
##  $ hispanic_per                   : num  53.8 45.9 55.4 59.4 56.9 NA 14.2 49.5 18.2 59.2 ...
##  $ white_per                      : num  1.7 2.3 10.4 3.6 1.6 NA 44.9 4.1 49.8 6.3 ...
##  $ grade1                         : num  NA NA NA NA NA NA 107 NA NA NA ...
##  $ grade2                         : num  NA NA NA NA NA NA 139 NA NA NA ...
##  $ grade3                         : num  NA NA NA NA NA NA 110 NA NA NA ...
##  $ grade4                         : num  NA NA NA NA NA NA 114 NA NA NA ...
##  $ grade5                         : num  NA NA NA NA NA NA 107 NA NA NA ...
##  $ grade6                         : num  32 NA 92 NA NA NA 149 NA NA NA ...
##  $ grade7                         : num  33 NA 73 NA NA NA 126 NA NA NA ...
##  $ grade8                         : num  50 NA 76 NA NA NA 117 NA NA NA ...
##  $ grade9                         : num  98 109 101 131 143 NA 117 5 184 50 ...
##  $ grade10                        : num  79 97 93 49 100 NA 123 89 162 63 ...
##  $ grade11                        : num  80 93 77 44 51 NA 147 59 128 38 ...
##  $ grade12                        : num  50 95 86 NA 73 NA 157 65 143 23 ...
##  $ perc_total                     : num  98.6 100 99.4 99.6 99.4 NA 98.6 99.5 98.2 99.4 ...

Creamos entonces un nuevo objeto sat_demog_dir con la función left_join que resulta de unir los colegios que aparecen en la base del SAT, con los datos demográficos que tienen en común el identificador del colegio DBN.

Un análisis simple

Con las bases unidas podemos ver como lucen las correlaciones entre la composición demográfica y los resultados en el SAT de matemáticas. Para ello utilizamos la función plot() de R base que nos permite ver los diagramas de dispersión.

En primer lugar podemos ver los resultados en matemáticas y el porcentaje de Afrodescendientes en los colegios.

plot(sat_demog_dir$SAT.Math.Avg..Score,sat_demog_dir$black_per)

Podemos hacer lo mismo con el porcentaje de blancos:

plot(sat_demog_dir$SAT.Math.Avg..Score,sat_demog_dir$white_per)

o de asiáticos:

plot(sat_demog_dir$SAT.Math.Avg..Score,sat_demog_dir$asian_per)

Próximos Pasos

En este vídeo tutorial hicimos una demostración de cómo podemos limpiar datos con la librería tidyverse y un análisis de correlación sencillo. Un recurso útil para profundizar los conocimientos es el libro R for Data Science. También profundizaremos sobre estos temas en las clases sincrónica complementarias.

Otros recursos útiles son:

  • Colin Gillespie and Robin Lovelace, 2017. Efficient R Programming, A Practical Guide to Smarter Programming Link
  • Chester Ismay and Albert Y. Kim., 2022. Statistical Inference via Data Science A ModernDive into R and the Tidyverse Link

  1. Aplican los “disclaimers” usuales. Si tenes comentarios, sugerencias, no dudes en enviarme un mensaje por Slack, serán muy bienvenidos y tenidos en cuenta para la calificación final.↩︎